<template>
  <div class="fs-estimated-virtuallist-container" ref="containerRef">
    <div
        class="fs-estimated-virtuallist-content"
        ref="contentRef"
        @mouseleave="leave"
        @mouseenter="enter"
        @mouseover="over"
        @scroll="handleScroll"
    >
      <div class="fs-estimated-virtuallist-list" ref="listRef" :style="scrollStyle">
        <div class="fs-estimated-virtuallist-list-item" ref="itemRef" v-for="(i, index) in renderList"
             :key="i[dataKey] || index">
          <slot name="item" :item="i"></slot>
        </div>
      </div>

      <div class="fs-estimated-virtuallist-list" :style="scrollStyle2">
        <div class="fs-estimated-virtuallist-list-item" v-for="(i, index) in virList" :key="i[dataKey] || index">
          <slot name="item" :item="i"></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import {
  type CSSProperties,
  computed,
  onMounted,
  reactive,
  ref,
  watch,
  nextTick,
  onBeforeUnmount,
} from 'vue';
import type {IPosInfo, VirtualStateType} from './data';


import {useDebounceFn,  useResizeObserver} from "@vueuse/core";

const props = defineProps({
  // 从data-sources中的每个数据对象获取唯一键。或者使用每个数据源调用函数并返回其唯一键。其值在数据源中必须是唯一的，用于标识每一项的尺寸。
  dataKey: {
    type: String,
    required: true,
    default: 'id'
  },
  // 是否加载中
  loading: {
    type: Boolean,
    required: true
  },
  // 为列表生成的源数组，每个数组数据必须是一个对象，并且具有唯一的key get或generate fordata key属性。
  dataSource: {
    type: Array,
    required: true
  },
  // 单个滚动完成的停留间隔 单位毫秒
  interval: {
    type: Number,
    required: false,
    default: 0
  },
  // 是否刷新数据 在数据源更新后按着新数据渲染 默认 false: 数据源更改后会累加数据 为 true则不会累加
  refresh: {
    type : Boolean,
    default: false
  }
});

const emit = defineEmits<{
  // 列表滚动到底部触发
  (e: 'scroll-end'): void
  // 单行滚动完成事件
  (e: 'line-scroll-end'): void
}>();

const propList = ref<{ [key: string]: any }[]>([]);

const contentRef = ref<HTMLDivElement>();

const listRef = ref<HTMLDivElement>();

const itemRef = ref<HTMLDivElement[]>();

const positions = ref<IPosInfo[]>([]);

const state = reactive<VirtualStateType>({
  viewHeight: 0,
  itemHeight: 0,
  startIndex: 0,
  maxCount: 1,
  preLen: 0,
  rafTimer: null,
  isHover: false
});

let timeOutId: any;

/**
 * @description 延迟一帧触发
 * @param callback 回调
 * @returns raf的id
 */
function delayRef(callback: () => void) {

  // 清除上次
  cancelAnimationFrame(state.rafTimer || 0)

  function frame() {
    state.rafTimer = requestAnimationFrame(callback);
  }

  const scrollTop = contentRef.value?.scrollTop || 0
  // 判断是否有停留时间
  if (props.interval > 0 && scrollTop > 0 && scrollTop % state.itemHeight === 0) {
    // console.log('停止')
    emit('line-scroll-end')
    clearTimeout(timeOutId)
    // 如果有停留时间则延迟指定时间执行下一步
    timeOutId = setTimeout(() => {

      state.rafTimer = requestAnimationFrame(frame);
    }, props.interval)
    return;
  }

  state.rafTimer = requestAnimationFrame(frame);
}

const endIndex = computed(() => Math.min(propList.value.length, state.startIndex + state.maxCount));

const renderList = computed(() => propList.value.slice(state.startIndex, endIndex.value));

const virList = computed(() => {
  // console.log('propList.value.length', propList.value.length)
  // console.log('state.maxCount', state.maxCount)
  // 如果当前的列表长度小于最大显示数量，则不进行无缝滚动
  if (propList.value.length < (state.maxCount -1)) {
    return [];
  }
  return  propList.value.slice(0, state.maxCount)
}); // 复制多几个用来无缝滚动的

const scrollStyle = computed(
    () => {
      return {
        height: `${state.itemHeight * renderList.value.length}px`,
        transform: `translate3d(0, ${state.itemHeight * state.startIndex}px, 0)`
      } as CSSProperties
    }
);

const scrollStyle2 = computed(
    () => {
      return {
        transform: `translate3d(0, ${state.itemHeight * state.startIndex}px, 0)`
      } as CSSProperties
    }
);

/**
 * @description 播放滚动
 */
const move = () => {
  if (state.isHover) {
    return;
  }
  // 加载中就停止
  if (props.loading) {
    _cancel();
    return;
  }
  if (contentRef.value) {

    let scrollTop = contentRef.value.scrollTop

    // 实际不会触发 只会触发handleScroll
    if (positions.value.length && scrollTop >= positions.value[positions.value.length - 1].bottom) {
      scrollTop = 0;
      if (!props.loading) {
        emit('scroll-end');

      }
    } else {
      scrollTop += 1

    }
    contentRef.value.scrollTop = scrollTop
  }
  delayRef(move);
};

/**
 * 取消滚动
 */
const _cancel = () => {
  state.rafTimer !== null && window.cancelAnimationFrame(state.rafTimer);
};

/**
 * 鼠标进入元素时暂停滚动
 */
const enter = () => {
  state.isHover = true; //关闭_move
  _cancel();
};

/**
 * 鼠标离开是时恢复滚动
 */
const leave = () => {
  state.isHover = false; //开启_move
  move();
};

/**
 * 鼠标进入元素时暂停滚动
 */
const over = () => {
  _cancel();
};

// raf节流
function rafThrottle(fn: Function) {
  let lock = false;
  return function (this: any, ...args: any[]) {
    if (lock) return;
    lock = true;
    window.requestAnimationFrame(() => {
      fn.apply(this, args);
      lock = false;
    });
  };
}

const handleScroll = rafThrottle(() => {
  if(!contentRef.value) {
    return
  }
  let {scrollTop} = contentRef.value;
  state.startIndex = Math.floor(scrollTop / state.itemHeight);
  if (renderList.value.length === 0) {
    contentRef.value.scrollTop = 0;
    if (!props.loading) {
      emit('scroll-end')
    }
  }
});

/**
 * @description 初始化
 */
const init = () => {
  state.viewHeight = contentRef.value ? contentRef.value.offsetHeight : 0;
};

/**
 * @description 将新拿到的列表赋值给propList
 */
const addNewList = () => {
  if(props.refresh){
    state.preLen = 0
    propList.value = []
  }

  const disLen = props.dataSource.length - state.preLen;
  for (let i = 0; i < disLen; i++) {
    const newIndex = i + state.preLen;
    propList.value.push(props.dataSource[newIndex]);
  }
  state.preLen = propList.value.length;
};

/**
 * @description 重新计算最大数
 */
const handleSetPosition = useDebounceFn(() => {
  state.viewHeight = contentRef.value ? contentRef.value.offsetHeight : 0;
  nextTick(() => {
    if (itemRef.value && itemRef.value[0]) {
      // 拿数组第一项计算最大
      const maxCount = Math.ceil(state.viewHeight / itemRef.value[0].offsetHeight) + 1;
      // 如果没计算出来则返回
      if(isNaN(maxCount)) {
        return
      }
      state.maxCount = Math.ceil(state.viewHeight / itemRef.value[0].offsetHeight) + 1;
      state.itemHeight = itemRef.value[0].offsetHeight;
    }
  });
}, 300)

/**
 * @description 监听容器宽高变化
 */
const observeContent = () => {
  if (contentRef.value) {
    useResizeObserver(contentRef, () => {
      handleSetPosition()
    })

  }
};

onMounted(() => {
  observeContent();
  init();
});

watch(
    () => props.dataSource.length,
    () => {
      // 接受到的列表高度变化
      addNewList();
      handleSetPosition();
    },
    {
      immediate: true,
    }
);

watch(
    () => props.loading,
    value => {
      // 加载完之后开始动画
      if (!value) {
        move();
      }
    },
    {
      immediate: true,
    }
);


import { useElementVisibility } from '@vueuse/core'
// 元素不可见时暂停
const containerRef = ref()
const targetIsVisible = useElementVisibility(containerRef)
// 是否第一次进入
let oneMounted = false
watch(targetIsVisible, (value)=> {

  // console.log('元素是否可见', value)
  if(!oneMounted) {
    oneMounted = true
    return
  }
  if(value) {
    // 可见时就恢复
    leave()
  }else {
    enter()
  }

})

// 页面不可见时暂停
import { useDocumentVisibility } from '@vueuse/core'
// 判断当前窗口的可见性 如果不可见则暂停滚动
const visibility = useDocumentVisibility()
watch(visibility, (v: DocumentVisibilityState) =>{
  // console.log('当前窗口可见性',v);
  // 窗口不可见就暂停
  if(v === 'hidden') {
    enter()
  }else {
    // 可见时就恢复
    leave()
  }
})

// 页面销毁前销毁取消滚动
onBeforeUnmount(_cancel)

</script>

<style scoped lang="scss">

::-webkit-scrollbar {
  display: none;
  background-color: transparent;
}

.fs-estimated-virtuallist {
  &-container {
    position: relative;
    width: 100%;
    height: 100%;
  }

  &-content {
    position: absolute;
    width: 100%;
    height: 100%;
    overflow: auto;
  }

  &-list-item {
    box-sizing: border-box;
    width: 100%;
  }
}
</style>
